using System;
using System.Web;
using System.Xml;
using System.Reflection;
using System.Collections;
using System.Collections.Specialized;
using Deepcode.Flux.Core.Extensions.Codons;
using Deepcode.Flux.Core.Services.XML;
using Deepcode.Flux.Core.Services.FileSystem;

namespace Deepcode.Flux.Core.Extensions
{
	/// <summary>
	/// The AddinTree represents codons (objects that build objects) in a hierarchical manner.
	/// The addin tree is initialised either at startup, or, in response to changes in your
	/// applications /Settings directory against any *.config files. The addin tree performs
	/// a number of actions:
	/// 
	/// Initialisation
	///		Scans all of the known namespaces (in codons.xml.config) for ICodon derived
	///		classes. Each of these is then added to the codon factory ready to be built
	///		when the tree is built.
	///		
	///		Next, it queries for all .addin.config files and
	///		
	///		a) Creates an addin object in the loaded addins structure
	///		b) parses the xml within the config file.
	///		
	///		Each xml tag in the config should generally have it's own codon that responds 
	///		to the tag name, and if it does, a codon object is created at the path location 
	///		specified in the addin tree.
	///		
	///	Object / Tree request
	///		At the appropriate time, your application will want the real objects that
	///		are represented by the addin tree. You can do this by accessing the addin tree's
	///		GetPath(string path) method, which returns a single codon node. You then invoke
	///		the BuildChildItems or BuildItem method on this to create the actual objects
	///		represented by the addin tree and return them.
	/// </summary>
	public class AddinTree
	{
		#region Singleton design pattern
		/// <summary>
		/// This will hold the single AddinTree object when it's created
		/// </summary>
		private static AddinTree _AddinTreeObject = null;
		/// <summary>
		/// This static property allows access (and creates if necessary) the addin tree object
		/// </summary>
		public static AddinTree Tree
		{
			get
			{
				// Create the addin tree if necessary
				if( _AddinTreeObject == null )
					CreateAddinTree();

				return _AddinTreeObject;
			}
		}
		/// <summary>
		/// Allows programmatic release of the addin tree, forcing it to be reloaded next time it is accessed.
		/// </summary>
		public static void ReleaseTree()
		{
			_AddinTreeObject = null;
		}
		/// <summary>
		/// The create tree method initialises the addin tree and loads the addins etc.
		/// </summary>
		private static void CreateAddinTree()
		{
			try
			{
				_AddinTreeObject = new AddinTree();
				_AddinTreeObject.LoadTree();
			}
			catch( Exception e )
			{
				// If an error occurs during loading, ensure the tree is forced to reload
				ReleaseTree();
				// then rethrow the exception
				throw e;
			}
		}
	
		/// <summary>
		/// Private constructor to prevent creation of this object
		/// </summary>
		private AddinTree()
		{
		}
		#endregion
		#region Private Fields
		/// <summary>
		/// Holds the codon factory used in this application
		/// </summary>
		private CodonFactory codonFactory = new CodonFactory();
		/// <summary>
		/// Holds a list of addins that have been loaded
		/// </summary>
		private ArrayList loadedAddins = new ArrayList();
		/// <summary>
		/// Holds the root node for the addin tree.
		/// </summary>
		private AddinTreeNode root = new AddinTreeNode();
		#endregion
		#region Public Properties
		/// <summary>
		/// Returns an arraylist of Addin objects
		/// </summary>
		public ArrayList Addins{ get{ return loadedAddins; }}
		/// <summary>
		/// Returns an arraylist of Addin objects that are updateable
		/// </summary>
		public ArrayList UpdatableAddins
		{
			get
			{
				ArrayList result = new ArrayList();
				foreach( Addin a in Addins )
				{
					if( a.GUID != Guid.Empty &&
						a.Version != "" &&
						a.InstallUpdateScript != "" )
						result.Add( a );
				}
				return result;
			}
		}
		#endregion
		#region Initialisation Methods
		/// <summary>
		/// Performs initialisation of the tree
		/// </summary>
		private void LoadTree()
		{
			// First, initialise the codon factory with the codons scanned 
			// from the assemblies and namespaces in /Settings/Codons.xml.config
			codonFactory.ImportNamespaces();
			
			// Load all of the addin's from the various directories
			// Order is:
			// ~/admin/settings/*
			// ~/admin/extensions/[dir]/settings/*
			// ~/extensions/[dir]/settings/*
			// ~/settings/*
			LoadAddinsFrom("~/admin/settings/");
			LoadAddinsFrom("~/admin/extensions/");
			LoadAddinsFrom("~/extensions/");
			LoadAddinsFrom("~/settings/");

			// Finally, setup the file system watchers
			this.CreateAndEnableAddinWatcher();
		}
		/// <summary>
		/// Loads all of the *.addin.config files from a particular directory
		/// </summary>
		/// <param name="WebPathDirectory"></param>
		private void LoadAddinsFrom(string WebPathDirectory)
		{
			FileScanService search = new FileScanService();
			StringCollection addinfiles = search.SearchFilesInWebPath(WebPathDirectory, "*.addin.config", true);

			foreach (string file in addinfiles)
			{
				Addin addin = new Addin(true);
				addin.LoadFrom(file);
				// Ensure addin doesn't already exist in the list. If it does, do nothing more. If it doesn't, add it in.
				if (!AddinLoaded(addin.GUID))
					loadedAddins.Add(addin);
			}
		}

		/// <summary>
		/// Determines if an addin has already been loaded or not
		/// </summary>
		/// <param name="addinID"></param>
		/// <returns></returns>
		private bool AddinLoaded( Guid addinID )
		{
			foreach( Addin addin in this.loadedAddins )
				if( addin.GUID == addinID ) return true;
			return false;
		}
		/// <summary>
		/// Creates (or finds) the add in tree node represented by the path specified
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		public AddinTreeNode CreatePath( AddinTreeNode localroot, string path )
		{
			if( path == null || path.Length == 0 )
				return root;

			string [] paths = path.Split('/');
			AddinTreeNode CurrentPath = localroot;
			int i = 0;
			
			while( i < paths.Length )
			{
				if( paths[i] != "" )
				{
					AddinTreeNode nextpath = (AddinTreeNode) CurrentPath.Children[paths[i]];
					if( nextpath == null )
					{
						nextpath = new AddinTreeNode();
						CurrentPath.Children[paths[i]] = nextpath;
					}
					CurrentPath = nextpath;
				}
				i++;
			}
			return CurrentPath;
		}
		/// <summary>
		/// Clears the add in tree at the location path specified
		/// </summary>
		/// <param name="path"></param>
		public void ClearPath( string path )
		{
			if( path == null || path.Length == 0 ) return;
			string [] paths = path.Split('/');
			AddinTreeNode CurrentPath = root;
			AddinTreeNode ParentPath = null;

			int i = 0;
			while( i < paths.Length )
			{
				if( paths[i] != "" )
				{
					AddinTreeNode nextpath = (AddinTreeNode) CurrentPath.Children[paths[i]];
					if( nextpath == null ) return;
					ParentPath = CurrentPath;
					CurrentPath = nextpath;
				}
				i++;
			}

			if (CurrentPath != null)
			{
				CurrentPath.Children.Clear();
			}

			if (ParentPath != null)
			{
				ParentPath.Children.Remove( paths[ paths.Length -1 ] );
			}
		}
		/// <summary>
		/// Adds the information cached in the extensions to the addin tree
		/// </summary>
		/// <param name="e"></param>
		public void AddExtensions(ArrayList extensions)
		{
			foreach( Extension e in extensions )
			{
				AddinTreeNode thisroot = CreatePath( root, e.Path );
				foreach( ICodon codon in e.CodonCollection )
				{
					AddinTreeNode thispath = CreatePath( thisroot, codon.ID );
					thispath.Codon = codon;
				}
			}
		}
		/// <summary>
		/// Creates a codon from the specified xml tag
		/// </summary>
		/// <param name="parent"></param>
		/// <param name="xmltag"></param>
		public ICodon CreateCodon( XmlElement xmltag )
		{
			// Create the codon
			ICodon c = this.codonFactory.CreateCodon( xmltag );
			// Load the tag values
			InitialiseCodonFromXML( c, xmltag );
			return c;
		}
		/// <summary>
		/// Loads the xml tag data from the xml source file into the codon
		/// </summary>
		/// <param name="codonObject"></param>
		/// <param name="xmltag"></param>
		private void InitialiseCodonFromXML( object codonObject, XmlElement xmltag )
		{
			XmlUtilityService xservice = new XmlUtilityService();

			Type currentType = codonObject.GetType();
			// Decend up the inheritance tree to load all fields with the values from xmltag
			while( currentType != typeof(object))
			{
				// Get all private instance fields
				FieldInfo [] fieldInfoArray = currentType.GetFields( BindingFlags.NonPublic | BindingFlags.Instance );
				// Loop over all of the fields and see if they have one of our special attributes associated with them
				foreach( FieldInfo fieldInfo in fieldInfoArray )
				{
					// If the field is marked with the xmlcodon attribute, load field from xml
					XmlCodonAttribute tagAttribute = (XmlCodonAttribute) Attribute.GetCustomAttribute( fieldInfo, typeof(XmlCodonAttribute));
					if( tagAttribute != null )
					{
						// Get value from the xml tag
						string attrValue = xservice.GetSafe( xmltag, tagAttribute.Name, null );
						// If not specified and it's marked as required - show the error
						if( attrValue == null && tagAttribute.IsRequired )
							throw new Exception(String.Format("{0} is required for the node {1}", tagAttribute.Name, xmltag.Name ));

						if( attrValue != null )
						{
							// Set the tag objects field value to the value specified in the attribute
							if( fieldInfo.FieldType.IsSubclassOf( typeof(System.Enum)))
								fieldInfo.SetValue( codonObject, Convert.ChangeType( Enum.Parse( fieldInfo.FieldType, attrValue), fieldInfo.FieldType));
							else
								fieldInfo.SetValue( codonObject, Convert.ChangeType( attrValue, fieldInfo.FieldType ));
						}
					}
					// If the field is marked with the xmlcodonarray attribute, load field from xml
					XmlCodonArrayAttribute tagArrayAttribute = (XmlCodonArrayAttribute) Attribute.GetCustomAttribute( fieldInfo, typeof( XmlCodonArrayAttribute));
					if( tagArrayAttribute != null )
					{
						// Load the value
						string attrValue = xservice.GetSafe( xmltag, tagArrayAttribute.Name, null );
						// If not specified, but required, throw an error
						if( attrValue == null && tagArrayAttribute.IsRequired )
							throw new Exception( String.Format("{0} is required for the node {1}", tagArrayAttribute.Name, xmltag.Name ));

						if( attrValue != null )
						{
							string [] attrValueArray = attrValue.Split(',');
							fieldInfo.SetValue( codonObject, attrValueArray );
						}
					}
				}
				currentType = currentType.BaseType;
			}
		}
		#endregion
		#region Addin filesystem observer
		private System.IO.FileSystemWatcher _AddinWatcherCore = null;
		private System.IO.FileSystemWatcher _AddinWatcherCoreExtensions = null;
		private System.IO.FileSystemWatcher _AddinWatcherExtensions = null;
		private System.IO.FileSystemWatcher _AddinWatcherLocal = null;
		/// <summary>
		/// Creates the file system watcher to monitor for changes to the .addin.config files
		/// within the /Settings/Addins directory. Any change forces a reload of the addin tree.
		/// TODO: The filesystem watcher does not track deletions at the moment.
		/// </summary>
		private void CreateAndEnableAddinWatcher()
		{
			string AddinDirectoryCore = HttpContext.Current.Server.MapPath("~/admin/Settings/");
			string AddinDirectoryCoreExtensions = HttpContext.Current.Server.MapPath("~/admin/extensions/");
			string AddinDirectoryExtensions = HttpContext.Current.Server.MapPath("~/extensions/");
			string AddinDirectoryLocal = HttpContext.Current.Server.MapPath("~/Settings/");

			// CREATE THE WATCHER FOR /ADMIN/SETTINGS
			if (System.IO.Directory.Exists(AddinDirectoryCore))
			{
				_AddinWatcherCore = new System.IO.FileSystemWatcher(AddinDirectoryCore, "*.addin.config");
				_AddinWatcherCore.NotifyFilter =
					System.IO.NotifyFilters.CreationTime |
					System.IO.NotifyFilters.FileName |
					System.IO.NotifyFilters.LastWrite |
					System.IO.NotifyFilters.Size |
					System.IO.NotifyFilters.DirectoryName;

				_AddinWatcherCore.IncludeSubdirectories = true;

				_AddinWatcherCore.Changed += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCore.Created += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCore.Deleted += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCore.Renamed += new System.IO.RenamedEventHandler(AddinSetDirtyRename);
			}
			// CREATE THE WATCHER FOR /ADMIN/EXTENSIONS
			if (System.IO.Directory.Exists(AddinDirectoryCoreExtensions))
			{
				_AddinWatcherCoreExtensions = new System.IO.FileSystemWatcher(AddinDirectoryCoreExtensions, "*.addin.config");
				_AddinWatcherCoreExtensions.NotifyFilter =
					System.IO.NotifyFilters.CreationTime |
					System.IO.NotifyFilters.FileName |
					System.IO.NotifyFilters.LastWrite |
					System.IO.NotifyFilters.Size |
					System.IO.NotifyFilters.DirectoryName;

				_AddinWatcherCoreExtensions.IncludeSubdirectories = true;

				_AddinWatcherCoreExtensions.Changed += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCoreExtensions.Created += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCoreExtensions.Deleted += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherCoreExtensions.Renamed += new System.IO.RenamedEventHandler(AddinSetDirtyRename);
			}
			// CREATE THE WATCHER FOR /EXTENSIONS
			if (System.IO.Directory.Exists(AddinDirectoryExtensions))
			{
				_AddinWatcherExtensions = new System.IO.FileSystemWatcher(AddinDirectoryExtensions, "*.addin.config");
				_AddinWatcherExtensions.NotifyFilter =
					System.IO.NotifyFilters.CreationTime |
					System.IO.NotifyFilters.FileName |
					System.IO.NotifyFilters.LastWrite |
					System.IO.NotifyFilters.Size |
					System.IO.NotifyFilters.DirectoryName;

				_AddinWatcherExtensions.IncludeSubdirectories = true;

				_AddinWatcherExtensions.Changed += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherExtensions.Created += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherExtensions.Deleted += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherExtensions.Renamed += new System.IO.RenamedEventHandler(AddinSetDirtyRename);
			}
			// CREATE THE WATCHER FOR /SETTINGS
			if (System.IO.Directory.Exists(AddinDirectoryLocal))
			{
				_AddinWatcherLocal = new System.IO.FileSystemWatcher(AddinDirectoryLocal, "*.addin.config");
				_AddinWatcherLocal.NotifyFilter =
					System.IO.NotifyFilters.CreationTime |
					System.IO.NotifyFilters.FileName |
					System.IO.NotifyFilters.LastWrite |
					System.IO.NotifyFilters.Size |
					System.IO.NotifyFilters.DirectoryName;

				_AddinWatcherLocal.IncludeSubdirectories = true;

				_AddinWatcherLocal.Changed += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherLocal.Created += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherLocal.Deleted += new System.IO.FileSystemEventHandler(AddinSetDirty);
				_AddinWatcherLocal.Renamed += new System.IO.RenamedEventHandler(AddinSetDirtyRename);
			}
			
			// START THE EVENT PROCESSORS
			if( _AddinWatcherCore != null ) _AddinWatcherCore.EnableRaisingEvents = true;
			if( _AddinWatcherCoreExtensions != null ) _AddinWatcherCoreExtensions.EnableRaisingEvents = true;
			if( _AddinWatcherExtensions != null ) _AddinWatcherExtensions.EnableRaisingEvents = true;
			if( _AddinWatcherLocal != null ) _AddinWatcherLocal.EnableRaisingEvents = true;
		}
		/// <summary>
		/// Sets the tree to dirty in response to a changed, created or deleted event.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void AddinSetDirty(object sender, System.IO.FileSystemEventArgs e)
		{
			// The tree requires a reload, so release it
			AddinTree.ReleaseTree();
		}
		/// <summary>
		/// Sets the tree to dirty in response to a rename event
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void AddinSetDirtyRename(object sender, System.IO.RenamedEventArgs e)
		{
			AddinTree.ReleaseTree();
		}
		#endregion
		#region Node retrieval
		/// <summary>
		/// Gets the addin tree node at the path specified. Throws an exception
		/// if the path could not be found...
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		public AddinTreeNode GetTreeNode(string path)
		{
			if (path == null || path.Length == 0) 
				return this.root;

			string [] splitpath = path.Split('/');
			AddinTreeNode currentPath = root;
			int i = 0;

			while( i < splitpath.Length )
			{
				if( splitpath[i] != "" )
				{
					AddinTreeNode nextPath = (AddinTreeNode) currentPath.Children[splitpath[i]];
					if( nextPath == null ) 
						throw new Exception("Path " + path + " not found in the addin tree");
					currentPath = nextPath;
				}
				i++;
			}
			return currentPath;
		}
		/// <summary>
		/// Gets the addin tree node at the path specified. If the node could not be
		/// found, this method returns NULL as opposed to the GetTreeNode method which
		/// throws an exception.
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		public AddinTreeNode GetTreeNodeNoThrow( string path )
		{
			try{ return GetTreeNode( path ); }
			catch{ return null; }
		}
		#endregion
		#region Utilities
		/// <summary>
		/// Looks for the addin with the specified Guid in the loaded addins list.
		/// </summary>
		/// <param name="addinGuid"></param>
		/// <returns></returns>
		public Addin GetAddin( System.Guid addinGuid )
		{
			foreach( Addin a in this.loadedAddins )
				if( a.GUID == addinGuid ) return a;

			return null;
		}
		#endregion
		#region Debugging information
		/// <summary>
		/// Returns a string collection with all known addins, codons and a dump of the addin tree structure
		/// </summary>
		/// <returns></returns>
		public StringCollection GetTreeDump()
		{
			StringCollection c = new StringCollection();
			c.Add("*********** ADDINS *****************");
			foreach (Addin a in this.Addins)
			{
				c.Add(
					String.Format("ADDIN: {0} {1} (guid:{2} author:{3} description;{4} copy:{5} updateatlogin:{6} iscore:{7} script:{8}",
						a.Name,
						a.Version,
						a.GUID,
						a.Author,
						a.Description,
						a.Copyright,
						a.CheckUpdatesAtLogin,
						a.IsCoreAddin,
						a.InstallUpdateScript));
			}
			c.Add("*********** CODONS *****************");
			// Dump out any known codons
			foreach (string key in codonFactory.codonHashtable.Keys)
			{
				CodonBuilder cb = (CodonBuilder) codonFactory.codonHashtable[key];
				c.Add(key + "::" + cb.TagName + "::" + cb.ClassName);
			}

			c.Add("*********** ADDIN TREE *************");
			DumpTree( ref c, root.Children, 0 );
			return c;
		}
		/// <summary>
		/// Dumps the children - used for debugging
		/// </summary>
		/// <param name="c"></param>
		/// <param name="children"></param>
		/// <param name="Depth"></param>
		private void DumpTree( ref StringCollection c, Hashtable children, int Depth )
		{
			foreach (string childkey in children.Keys)
			{
				AddinTreeNode child = ((AddinTreeNode) children[childkey]);
				
				string output = "";
				for( int x=0; x<Depth; x++ ) output += "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
				output += childkey;
				if( child.Codon == null )
					output += " (PATH)";
				else
					output += String.Format(" (CODON {0} {1}) ", child.Codon.ID, child.Codon.Class);

				c.Add( output );
				// output children
				DumpTree( ref c, child.Children, Depth + 1 );
			}
		}
		#endregion
	}
}
